import type { Chain, PluginItem } from 'ssr-types'
import type { Compiler } from 'webpack'
import type { Module, Rule } from 'webpack-chain'
import { resolve } from 'path'
import * as webpack from 'ssr-webpack4'
import {
	judgeFramework,
	loadModuleFromFramework,
	loadModuleFromWebpack,
	getPkgMajorVersion,
	loadConfig,
	logWarning,
	getImageOutputPath,
	nameSpaceBuiltinModules,
	getBuildConfig,
	getDefineEnv
} from 'ssr-common-utils'
import { nodeExternals } from './externals'
import { FileToChunkRelationPlugin } from './plugins'
import { setStyle } from './setStyle'

const MiniCssExtractPlugin = require(loadModuleFromWebpack('ssr-mini-css-extract-plugin'))
const WebpackBar = require('webpackbar')

const [antdVersion, vantVersion] = [getPkgMajorVersion('antd'), getPkgMajorVersion('vant')]
const isAntd4 = antdVersion === 4
if (antdVersion === 5) {
	logWarning('Check antd@5.x has been installed, antd@4.x is more recommend in ssr environment')
}
const addBabelLoader = (chain: Rule<Module>, envOptions: any, isServer: boolean) => {
	const framework = judgeFramework()
	const { babelOptions, isDev } = loadConfig()
	let plugins: PluginItem[] = []
	let presets: PluginItem[] = []
	if (['ssr-plugin-react'].includes(framework)) {
		plugins = [
			[
				loadModuleFromWebpack('@babel/plugin-transform-runtime'),
				{
					regenerator: false,
					corejs: false,
					helpers: true
				}
			],
			[loadModuleFromWebpack('@babel/plugin-proposal-private-methods'), { loose: true }],
			[loadModuleFromWebpack('@babel/plugin-proposal-private-property-in-object'), { loose: true }]
		]
		if (isAntd4) {
			plugins.push([
				loadModuleFromWebpack('babel-plugin-import'),
				{
					libraryName: 'antd',
					libraryDirectory: 'lib',
					style: true
				},
				'antd'
			])
		}
		if (!isServer && isDev) {
			plugins.push(loadModuleFromFramework('react-refresh/babel'))
		}
		presets = [
			[loadModuleFromWebpack('@babel/preset-env'), envOptions],
			[
				loadModuleFromFramework('babel-preset-react-app'),
				{
					flow: false,
					typescript: true,
					runtime: 'automatic'
				}
			]
		]
	}
	if (['ssr-plugin-vue3', 'ssr-plugin-vue'].includes(framework)) {
		plugins = [
			[
				loadModuleFromWebpack('@babel/plugin-transform-runtime'),
				{
					corejs: false
				}
			],
			[
				loadModuleFromWebpack('babel-plugin-import'),
				{
					libraryName: 'ant-design-vue',
					libraryDirectory: 'lib',
					style: true
				},
				'ant-design-vue'
			]
		]
		if (vantVersion && vantVersion < 4) {
			plugins.push([
				loadModuleFromWebpack('babel-plugin-import'),
				{
					libraryName: 'vant',
					libraryDirectory: 'lib',
					style: true
				},
				'vant'
			])
		}
		if (framework === 'ssr-plugin-vue3') {
			plugins.push(loadModuleFromFramework('@vue/babel-plugin-jsx'))
		}
		presets = [
			[
				loadModuleFromWebpack('@babel/preset-typescript'),
				{
					isTSX: true,
					allExtensions: true
				}
			],
			[loadModuleFromWebpack('@babel/preset-env'), envOptions]
		]
	}
	plugins.push([loadModuleFromWebpack('@babel/plugin-proposal-optional-chaining')])
	plugins.push(...(babelOptions?.plugins ?? []))
	presets.push(...(babelOptions?.presets ?? []))
	plugins = plugins.filter(Boolean)
	presets = presets.filter(Boolean)
	chain
		.use('babel-loader')
		.loader(loadModuleFromWebpack('babel-loader'))
		.options({
			cacheDirectory: true,
			cacheCompression: false,
			sourceType: 'unambiguous',
			presets,
			plugins
		})
		.end()
}
const addCommonChain = (chain: Chain, isServer: boolean) => {
	const { babelOptions, corejsOptions, babelExtraModule, assetsDir, isDev, clientPrefix, cwd, whiteList, define } =
		loadConfig()
	const { publicPath, imagePath } = getImageOutputPath()
	const envOptions = {
		modules: false,
		...corejsOptions
	}
	if (process.env.NOMINIFY) {
		chain.optimization.minimize(false)
	}
	chain.devtool(((isServer ? process.env.SERVER_SOURCEMAP : process.env.CLIENT_SOURCEMAP) as any) ?? false)
	if (!isServer) {
		chain.externals(
			nodeExternals({
				isServer
			})
		)
	} else {
		const modulesDir = [resolve(cwd, './node_modules')]
		chain.externals(
			nodeExternals({
				isServer,
				whitelist: whiteList,
				// externals Dir contains example/xxx/node_modules ssr/node_modules
				modulesDir
			})
		)
	}
	if (clientPrefix && !isServer) {
		// for micro-app sourcemap
		const BannerPlugin = require(loadModuleFromFramework('ssr-webpack4')).BannerPlugin
		chain.plugin('BannerPlugin').use(
			new BannerPlugin({
				banner: () => '//# sourceURL=[file]',
				raw: true,
				include: /\.js$/
			})
		)
		const { SourceMapSource, RawSource } = require(loadModuleFromFramework('webpack-sources'))

		const microheader =
			'(function anonymous(\n) {\n;(function(proxyWindow){with(proxyWindow.__MICRO_APP_WINDOW__){(function(window,self,globalThis,document,Document,Array,Object,String,Boolean,Math,Number,Symbol,Date,Function,Proxy,WeakMap,WeakSet,Set,Map,Reflect,Element,Node,RegExp,Error,TypeError,JSON,isNaN,parseFloat,parseInt,performance,console,decodeURI,encodeURI,decodeURIComponent,encodeURIComponent,navigator,undefined,location,history){})'

		class BatchReplacePlugin {
			apply(compiler: Compiler) {
				compiler.hooks.compilation.tap('BatchReplacePlugin', (compilation) => {
					compilation.hooks.optimizeChunkAssets.tap('BatchReplacePlugin', (assets) => {
						assets.forEach((chunk) => {
							chunk.files.forEach((filename) => {
								if (filename.includes('.js')) {
									const asset = compilation.assets[filename]
									const originalSource = asset.source()
									const originalSourceMap = asset.map()
									const newSource = originalSource.replace(microheader, '')
									if (originalSourceMap) {
										compilation.updateAsset(filename, new SourceMapSource(newSource, filename, originalSourceMap))
									} else {
										compilation.updateAsset(filename, new RawSource(newSource))
									}
								}
							})
						})
					})
				})
			}
		}
		if (isDev) {
			chain.plugin('BannerMicro').use(BannerPlugin, [
				{
					banner: microheader,
					raw: true,
					test: /\.js($|\?)/i
				}
			])
			chain.plugin('BatchReplace').use(BatchReplacePlugin)
		}
	}
	const babelModule = chain.module
		.rule('compileBabel')
		.test(/\.(js|mjs|jsx|ts|tsx)$/)
		.exclude.add(/node_modules|core-js/)
		.add((babelOptions?.exclude as Array<string | RegExp>) ?? [])
		.end()

	chain.module.rule('mjs').test(/\.mjs/).type('javascript/auto').end()

	const module = chain.module.rule('compileBabelForExtraModule').test(/\.(js|mjs|jsx|ts|tsx)$/).include

	const babelExtraModuleChain = module
		.add(babelExtraModule ?? [])
		.add((babelOptions?.include as Array<string | RegExp>) ?? [])
		.end()
		.exclude.add(/core-js/)
		.end()

	addBabelLoader(babelModule, envOptions, isServer)
	addBabelLoader(babelExtraModuleChain, envOptions, isServer)
	chain.module
		.rule('images')
		.test(/\.(jpe?g|png|svg|gif)(\?[a-z0-9=.]+)?$/)
		.use('url-loader')
		.loader(loadModuleFromWebpack('url-loader'))
		.options({
			name: '[name].[hash:8].[ext]',
			// require 图片的时候不用加 .default
			esModule: false,
			limit: 4096,
			fallback: {
				loader: 'file-loader',
				options: {
					emitFile: !isServer,
					publicPath,
					name: '[name].[hash:8].[ext]',
					esModule: false,
					outputPath: imagePath
				}
			}
		})
		.end()
	chain.module
		.rule('fonts')
		.test(/\.(eot|woff|woff2|ttf)(\?.*)?$/)
		.use('file-loader')
		.loader(loadModuleFromWebpack('file-loader'))
		.options({
			name: `${assetsDir}/[name].[hash:8].[ext]`,
			esModule: false,
			emitFile: !isServer
		})
	const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
	const generateAnalysis = Boolean(process.env.GENERATE_ANALYSIS)

	setStyle(chain, /\.css$/, {
		rule: 'css',
		importLoaders: 1,
		isServer
	}) // 设置css

	setStyle(chain, /\.less$/, {
		rule: 'less',
		loader: 'less-loader',
		importLoaders: 2,
		isServer
	})

	chain.plugin('minify-css').use(MiniCssExtractPlugin, getBuildConfig().cssBuildConfig)

	chain.plugin('webpackBar').use(
		new WebpackBar({
			name: isServer ? 'server' : 'client',
			color: isServer ? '#f173ac' : '#45b97c'
		})
	)
	chain.plugin('ssrDefine').use(webpack.DefinePlugin, [
		{
			...getDefineEnv(),
			...process.env,
			__isBrowser__: !isServer,
			__VUE_OPTIONS_API__: true,
			__VUE_PROD_DEVTOOLS__: false,
			...(isServer ? define?.server : define?.client),
			...define?.base
		}
	])

	if (!isServer) {
		nameSpaceBuiltinModules.forEach((moduleName) => {
			chain.node.set(moduleName, 'empty')
		})
		chain.when(Boolean(isDev && clientPrefix), (chain) => {
			chain.plugin('chunkMap').use(FileToChunkRelationPlugin)
		})
		chain.when(generateAnalysis, (chain) => {
			chain.plugin('analyze').use(BundleAnalyzerPlugin)
		})
	}
}

export { addCommonChain }
